package organizer;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import javax.swing.JFileChooser;
import org.apache.log4j.Logger;

/**
 * Server side of organizer.
 * @author dawong
 */
public class Organizer {

    private static final Logger log = Logger.getLogger(Organizer.class);

    private List<File> sourceDirs = new ArrayList<File>();
    private File trashDir = null;
    private List<Duplicate> dupList;
    private IOrganizerService orgService;

    public Organizer() {
//        orgService = new DerbyOrganizerService();
//        orgService = new HibernateOrganizerService();
        ServiceLoader<IOrganizerService> loader = ServiceLoader.load(IOrganizerService.class);
        for (IOrganizerService service : loader) {
            orgService = service;
            break;
        }
        log.info("Using service implementation: " + orgService);
    }

    public List<File> getSourceDirs() {
        return sourceDirs;
    }

    public void setSourceDirs(List<File> sourceDirs) {
        this.sourceDirs = sourceDirs;
    }

    public void addSourceDir(File sourceDir) {
        sourceDirs.add(sourceDir);
    }

    public File getTrashDir() {
        return trashDir;
    }

    public void setTrashDir(File trashDir) {
        this.trashDir = trashDir;
    }

    public void scanFiles(final IProgressMonitor progress) {
        log.info("counting files");
        dupList = null;
        orgService.scanFiles(sourceDirs, progress);
    }
    
    public List<String> getDuplicateDirectories() {
        log.info("getting duplicate directories");
        List<Duplicate> dups = getDuplicates(null);
        List<String> ret = Collections.emptyList();
        Set<String> dirs = new HashSet<String>();
        for (Duplicate dup : dups) {
            for (String path : dup.getFiles()) {
                File f1 = new File(path);
                dirs.add(f1.getParent());
            }
        }
        if (dirs.size() > 0) {
            ret = new ArrayList<String>();
            ret.addAll(dirs);
        }
        Collections.sort(ret);
        return ret;
    }

    public void calcDuplicates(IProgressMonitor progress) {
        log.info("calc duplicates");
        orgService.calcDuplicates(progress);
    }

    public List<Duplicate> getDuplicates(IProgressMonitor progress) {
        if (dupList == null) {
            log.info("getting duplicates");
            dupList = orgService.getDuplicates(progress);
        }
        return dupList;
    }

    /**
     * Select the files to be removed based on the directory priority.  The 
     * files in higher-ordered directories will be kept.
     * @param directoryPriorityList Files in higher-ordered (closer to index 0)
     * directories will be kept.
     * @return The files to delete.
     */
    public Set<String> selectDuplicates(List<String> directoryPriorityList) {
        log.info("selecting duplicates");
        Set<String> deleted = new HashSet<String>();
        List<Integer> reverseIndex = new ArrayList<Integer>();
        for (int i = 0; i < directoryPriorityList.size(); i++) {
            reverseIndex.add(i);
        }
        List<String> sortedPriorityList = new ArrayList<String>(directoryPriorityList);
        quicksort(sortedPriorityList, reverseIndex, 0, directoryPriorityList.size()-1);
        log.debug("sorted dirs: " + sortedPriorityList);
        List<Duplicate> dups = getDuplicates(null);
        for (Duplicate dup : dups) {
            List<File> files = new ArrayList<File>();
            for (String path : dup.getFiles()) {
                files.add(new File(path));
            }
            int minIndex = -1;
            File selectedFile = null;
            for (File file : files) {
                String dir = file.getParent();
                int tmpIndex = Collections.binarySearch(sortedPriorityList, dir);
                int index = reverseIndex.get(tmpIndex);
                if (minIndex < 0 || index < minIndex) {
                    selectedFile = file;
                    minIndex = index;
                }
            }
            for (File file : files) {
                if (file != selectedFile) {
                    deleted.add(file.getAbsolutePath());
                }
            }
        }
        return deleted;
    }

    public void removeDuplicates(Set<String> deletes) {
        log.info("removing duplicates");
        for (String delete : deletes) {
            File deleteFile = new  File(delete);
            if (deleteFile.exists()) {
                int idx = delete.indexOf(File.separator);
                delete = delete.substring(idx+1);
                File trashFile = new File(trashDir, delete);
                log.info("Moving " + deleteFile + " to " + trashFile.getAbsolutePath());
                if (!trashFile.getParentFile().exists()) {
                    if (!trashFile.getParentFile().mkdirs())
                        log.error("Can not create dir: " + trashFile);
                }
                deleteFile.renameTo(trashFile);
            }
        }
    }

    /**
     * Write duplicates to {@code exportFile}.
     * @param exportFile 
     * @param paths Absolute paths to files.
     */
    public void exportResults(File exportFile, List<String> paths) {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(exportFile));
            for (String path : paths) {
                writer.write(path);
                writer.newLine();
            }
            writer.flush();
        } catch (Exception e) {
            log.debug(null, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (Exception e) {
                    log.debug(null, e);
                }
            }
        }
    }
    
    private void quicksort(List<? extends Comparable> valueList, List<Integer> indexList, int left, int right) {
        if (right > left) {
            int pivot = left + (right - left) / 2;
            int newPivot = partition(valueList, indexList, left, right, pivot);
            quicksort(valueList, indexList, left, newPivot-1);
            quicksort(valueList, indexList, newPivot+1, right);
        }

    }

    private int partition(List<? extends Comparable> valueList, List<Integer> indexList, int left, int right, int pivot) {
        Comparable value = valueList.get(pivot);
        Collections.swap(valueList, pivot, right);
        Collections.swap(indexList, pivot, right);
        int store = left;
        for (int i = left; i < right; i++) {
            Comparable currVal = valueList.get(i);
            if (currVal.compareTo(value) <= 0) {
                Collections.swap(valueList, i, store);
                Collections.swap(indexList, i, store);
                store++;
            }
        }
        Collections.swap(valueList, store, right);
        Collections.swap(indexList, store, right);
        return store;
    }

    public void run(RunOptions options) {
        scanFiles(null);
        calcDuplicates(null);
        List<Duplicate> dups = getDuplicates(null);
        for (Duplicate dup : dups) {
            log.info(dup);
        }
        List<String> dirs = getDuplicateDirectories();
        for (String dir : dirs) {
            log.info(dir);
        }
        switch (options.getResultsAction()) {
            case EXPORT_ALL:
                log.info("Writing all duplicates to " + options.getExportPath());
                Set<String> selected = selectDuplicates(dirs);
                exportResults(new File(options.getExportPath()), new ArrayList<String>(selected));
                break;
            case EXPORT_SELECTED:
                log.info("Writing auto-selected duplicates to delete to " + options.getExportPath());
                List<String> allDups = new ArrayList<String>();
                for (Duplicate dup : dups) {
                    allDups.addAll(dup.getFiles());
                }
                exportResults(new File(options.getExportPath()), allDups);
                break;
            case MOVE_TO_TRASH:
                log.info("Moving auto-selected duplicates to delete to " + options.getTrashDir());
                setTrashDir(new File(options.getTrashDir()));
                Set<String> deletes = selectDuplicates(dirs);
                removeDuplicates(deletes);
                break;
        }
    }

    public static void main(String[] args) {
        if (args.length > 0) {
            Organizer organizer = new Organizer();
            RunOptions options = new RunOptions();
            options.setResultsAction(RunOptions.Action.EXPORT_ALL);
            String defaultExportPath = new File(System.getProperty("user.home"), "dup_results.txt").getAbsolutePath();
            options.setExportPath(defaultExportPath);
            for (int i = 0; i < args.length; i++) {
                String arg = args[i];
                if (arg.startsWith("-")) {
                    // Found a switch
                    if (arg.equals("--export-all")) {
                        options.setResultsAction(RunOptions.Action.EXPORT_ALL);
                        if (i+1 < args.length) {
                            String exportPath = args[i + 1];
                            options.setExportPath(exportPath);
                        } else {
                            throw new IllegalArgumentException("--export-all requires argument for the export file");
                        }
                    } else if (arg.equals("--export-selected")) {
                        options.setResultsAction(RunOptions.Action.EXPORT_SELECTED);
                        if (i+1 < args.length) {
                            String exportPath = args[i + 1];
                            options.setExportPath(exportPath);
                        } else {
                            throw new IllegalArgumentException("--export-selected requires argument for the export file");
                        }
                    } else if (arg.equals("--trash")) {
                        options.setResultsAction(RunOptions.Action.MOVE_TO_TRASH);
                        if (i+1 < args.length) {
                            String trashDirPath = args[i + 1];
                            options.setTrashDir(trashDirPath);
                        } else {
                            throw new IllegalArgumentException("--trash requires argument for the export file");
                        }
                    }
                } else {
                    // Assume scan dir.
                    organizer.addSourceDir(new File(arg));
                }
            }
            organizer.run(options);
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append("Give the list of directories to scan.\n");
            sb.append("Options:\n");
            sb.append("  --export-all <export file>\n");
            sb.append("      Write all the duplicates to the export file\n");
            sb.append("  --export-selected <export file>\n");
            sb.append("      Write only the auto-selected duplicates to the export file\n");
            sb.append("  --trash <trash dir>\n");
            sb.append("      Move the auto-selected duplicates to the trash dir\n");
            System.out.println(sb.toString());
        }
    }

}
